<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0" />
    <title>使用渐变背景实现一个颜色选择器</title>
    <meta
      name="keywords"
      content="demo, js, css, color-picker" />
    <style>
      body {
        background-color: #fff;

        --alpha-bg-size: 10px;
      }

      .alpha-bg {
        background-color: #fff;
        background-image: linear-gradient(45deg, lightgray 25%, transparent 25%, transparent 75%, lightgray 75%),
          linear-gradient(45deg, lightgray 25%, transparent 25%, transparent 75%, lightgray 75%);
        background-position: 0 0, calc(var(--alpha-bg-size) / 2) calc(var(--alpha-bg-size) / 2);
        background-size: var(--alpha-bg-size) var(--alpha-bg-size);
      }

      .flex {
        display: flex;
      }

      .color-picker-container {
        margin: 0 auto;
        padding: 0.5rem;
        max-width: 90vw;
        width: 400px;
        height: fit-content;
        border-radius: 0.25rem;
        background-color: #fff;
        box-shadow: 0 7px 30px rgba(100, 100, 111, 0.2);
      }

      .color-picker-saturation-wrap {
        position: relative;
        flex: 1;
        margin-right: 0.5rem;
        padding-bottom: calc(100% - var(--alpha-bg-size) * 2 - 0.5rem);
      }

      .color-picker-saturation-mask {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        cursor: pointer;
      }

      .color-picker-saturation-mask-white {
        background: linear-gradient(90deg, #fff, transparent);
      }

      .color-picker-saturation-mask-black {
        background: linear-gradient(0deg, #000, transparent);
      }

      .color-picker-saturation-pointer {
        position: absolute;
        top: 0;
        left: 100%;
        width: 4px;
        height: 4px;
        border-radius: 50%;
        box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px rgba(0, 0, 0, 0.3), 0 0 1px 2px rgba(0, 0, 0, 0.4);
        cursor: pointer;
        transform: translate(-50%, -50%);
        pointer-events: none;
      }

      .color-picker-color-slider {
        position: relative;
        width: calc(var(--alpha-bg-size) * 2);
        border-radius: 0.125rem;
        background: linear-gradient(180deg, red 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red);
        cursor: pointer;
      }

      .color-picker-alpha-slider-container {
        display: flex;
        margin-top: 0.5rem;
      }

      .color-picker-alpha-slider {
        position: relative;
        overflow-x: hidden;
        flex: 1;
        height: calc(var(--alpha-bg-size) * 2);
        border-radius: 0.125rem;
        cursor: pointer;
      }

      .color-picker-alpha-slider-gradient {
        width: 100%;
        height: 100%;
      }

      .color-picker-slider-pointer {
        position: absolute;
        border-radius: 1px;
        background: #fff;
        box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
        cursor: pointer;
        pointer-events: none;
      }

      .color-picker-alpha-slider-pointer {
        top: 1px;
        left: calc(100% - 4px);
        width: 4px;
        height: calc(100% - 2px);
      }

      .color-picker-color-slider-pointer {
        top: 0;
        left: 1px;
        width: calc(100% - 2px);
        height: 4px;
      }

      .color-picker-preview-container {
        margin-left: 0.5rem;
        width: calc(var(--alpha-bg-size) * 2);
        height: calc(var(--alpha-bg-size) * 2);
        border-radius: 0.125rem;
      }

      .color-picker-preview {
        width: 100%;
        height: 100%;
        border-radius: 0.125rem;
      }
    </style>
  </head>
  <body>
    <h1 style="text-align: center">使用渐变背景实现一个颜色选择器</h1>
    <div class="color-picker-container">
      <div class="flex">
        <div class="color-picker-saturation-wrap color-saturation-wrap">
          <div class="color-picker-saturation-mask color-picker-saturation-mask-white"></div>
          <div class="color-picker-saturation-mask color-picker-saturation-mask-black"></div>
          <div class="color-picker-saturation-pointer"></div>
        </div>
        <div class="color-picker-color-slider">
          <div class="color-picker-color-slider-pointer color-picker-slider-pointer"></div>
        </div>
      </div>
      <div class="color-picker-alpha-slider-container">
        <div class="color-picker-alpha-slider alpha-bg">
          <div class="color-picker-alpha-slider-gradient"></div>
          <div class="color-picker-alpha-slider-pointer color-picker-slider-pointer"></div>
        </div>
        <div class="color-picker-preview-container alpha-bg preview-container">
          <div class="color-picker-preview"></div>
        </div>
      </div>
    </div>
    <h2
      style="text-align: center"
      class="rgba-output"></h2>
    <h2
      style="text-align: center"
      class="hex-output"></h2>
    <h2
      style="text-align: center"
      class="hsv-output"></h2>

    <script>
      // 获取DOM元素
      const saturationWrap = document.querySelector('.color-saturation-wrap') // 饱和度包裹
      const colorSlider = document.querySelector('.color-picker-color-slider') // 颜色滑块
      const alphaSlider = document.querySelector('.color-picker-alpha-slider') // 透明度滑块
      const alphaSliderGradient = document.querySelector('.color-picker-alpha-slider-gradient') // 透明度渐变
      const colorPreview = document.querySelector('.preview-container .color-picker-preview') // 颜色预览
      const rgbaOutput = document.querySelector('.rgba-output') // 显示RGBA值
      const hexOutput = document.querySelector('.hex-output') // 显示Hex值
      const hsvOutput = document.querySelector('.hsv-output') // 显示HSV值
      const alphaSliderPointer = document.querySelector('.color-picker-alpha-slider-pointer') // 透明度指针
      const colorSliderPointer = document.querySelector('.color-picker-color-slider-pointer') // 颜色指针
      const saturationWrapPointer = document.querySelector('.color-picker-saturation-pointer') // 饱和度指针

      // 初始化色调、饱和度、亮度和透明度
      let hue = 0,
        saturation = 1,
        value = 1,
        alpha = 1

      // 限制数值在min和max之间
      const clamp = (value, min, max) => Math.min(Math.max(value, min), max)

      // 更新颜色预览及输出
      const updateColorPreview = () => {
        const rgb = hsvToRgb(hue, saturation, value) // 将HSV转换为RGB
        colorPreview.style.backgroundColor = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})` // 更新预览背景
        rgbaOutput.textContent = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})` // 更新RGBA输出
        hexOutput.textContent = rgbaToHex(rgb.r, rgb.g, rgb.b, alpha) // 更新Hex输出
        hsvOutput.textContent = `hsv(${hue}, ${Math.round(saturation * 100)}%, ${Math.round(value * 100)}%)` // 更新HSV输出
        updateAlphaSlider(rgb) // 更新透明度滑块
        return rgb // 返回RGB值
      }

      // 更新饱和度包裹的背景色
      const updateSaturationWrap = ({ r, g, b }) => {
        saturationWrap.style.backgroundColor = `rgb(${r}, ${g}, ${b})` // 使用当前RGB值更新背景
      }

      // 更新透明度滑块的背景渐变
      const updateAlphaSlider = ({ r, g, b }) => {
        alphaSliderGradient.style.background = `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0) 0%, rgba(${r}, ${g}, ${b}, 1) 100%)` // 从透明到不透明的渐变
      }

      // HSV 转 RGB
      const hsvToRgb = (h, s, v) => {
        let r, g, b
        const i = Math.floor(h / 60) % 6
        const f = h / 60 - Math.floor(h / 60)
        const p = v * (1 - s)
        const q = v * (1 - f * s)
        const t = v * (1 - (1 - f) * s)

        // 根据色相计算RGB值
        switch (i) {
          case 0:
            r = v
            g = t
            b = p
            break
          case 1:
            r = q
            g = v
            b = p
            break
          case 2:
            r = p
            g = v
            b = t
            break
          case 3:
            r = p
            g = q
            b = v
            break
          case 4:
            r = t
            g = p
            b = v
            break
          case 5:
            r = v
            g = p
            b = q
            break
        }

        return {
          r: Math.round(r * 255), // 将值缩放到0-255
          g: Math.round(g * 255),
          b: Math.round(b * 255)
        }
      }

      // RGBA 转 Hex
      const rgbaToHex = (r, g, b, a) => {
        const alpha = Math.round(a * 255) // 将alpha值转换为0-255
        return `#${((1 << 24) + (r << 16) + (g << 8) + b + (alpha << 24)).toString(16).slice(1)}` // 通过位运算生成Hex字符串
      }

      // 更新饱和度和亮度
      const updateSaturation = (event) => {
        const { offsetX, offsetY } = event // 获取鼠标相对位置
        const { width, height } = saturationWrap.getBoundingClientRect() // 获取包裹的宽高
        saturation = clamp(offsetX / width, 0, 1) // 计算饱和度
        value = clamp(1 - offsetY / height, 0, 1) // 计算亮度
        saturationWrapPointer.style.left = `${offsetX}px` // 更新指针位置
        saturationWrapPointer.style.top = `${offsetY}px`
        updateColorPreview() // 更新颜色预览
      }

      // 更新色调
      const updateHue = (event) => {
        const { offsetY } = event // 获取鼠标y坐标
        const { height } = colorSlider.getBoundingClientRect() // 获取颜色滑块高度
        hue = Math.round((offsetY / height) * 360) % 360 // 计算色调
        colorSliderPointer.style.top = `${offsetY}px` // 更新指针位置
        let rgb = updateColorPreview() // 更新预览并获取当前RGB
        updateSaturationWrap(rgb) // 更新饱和度背景
      }

      // 更新透明度
      const updateAlpha = (event) => {
        const { offsetX } = event // 获取鼠标X坐标
        const { width } = alphaSlider.getBoundingClientRect() // 获取透明度滑块宽度
        alpha = clamp(offsetX / width, 0, 1) // 计算透明度
        alphaSliderPointer.style.left = `${offsetX}px` // 更新指针位置
        updateColorPreview() // 更新颜色预览
      }

      // 事件监听器
      saturationWrap.addEventListener('click', updateSaturation) // 点击饱和度区域
      colorSlider.addEventListener('click', updateHue) // 点击色调滑块
      alphaSlider.addEventListener('click', updateAlpha) // 点击透明度滑块

      const init = () => {
        updateColorPreview() // 初始化颜色预览
        updateSaturationWrap({ r: 255, g: 0, b: 0 }) // 初始化饱和度包裹背景色
      }
      init() // 调用初始化函数
    </script>
  </body>
</html>
